package com.nulldev.util.JVM;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import com.nulldev.util.JVM.arguments.Argument;
import com.nulldev.util.JVM.arguments.ArgumentException;
import com.nulldev.util.JVM.arguments.ArgumentWithValue;
import com.nulldev.util.JVM.arguments.BaseArgument;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;

public class Arguments {
	/*
	 * As of build 80.0, Arguments API v1 has been removed.
	 */

	private static final Logger log = LoggerFactory.getLogger();
	private static final Arguments args = new Arguments();
	private static final variable<ParsedArguments> _pargs = new variable<ParsedArguments>();

	public static interface ArgumentInit {
		public void onInit(final Arguments args);
	}

	public static void argumentInit(final ArgumentInit init) {
		init.onInit(args);
	}

	/** A parsed argument with and optional value. */
	public static class ParsedArgument {
		private final BaseArgument argument;
		private final Object value;

		<T> ParsedArgument(ArgumentWithValue<T> argument, T value) {
			this.argument = argument;
			this.value = value;
		}

		ParsedArgument(Argument argument) {
			this.argument = argument;
			this.value = null;
		}

		/** Returns whether the argument is of the given form. **/
		public boolean is(String form) {
			for (final String argForm : argument.getForms()) {
				if (argForm.equals(form))
					return true;
			}
			return false;
		}

		/**
		 * Returns whether the argument is equal in identity (==) to the parsed
		 * argument.
		 **/
		public boolean is(BaseArgument argument) {
			return argument == this.argument;
		}

		/**
		 * Returns the parsed value for the argument, or throws an ArgumentException if
		 * the parsed argument and given argument are not the same.
		 **/
		@SuppressWarnings("unchecked")
		public <T> T getValue(ArgumentWithValue<T> argument) {
			if (argument != this.argument)
				throw new ArgumentException(
						"The provided argument " + argument.getForms()[0] + " does not match the parsed argument " + this.argument.getForms()[0] + ".");
			return (T) value;
		}

		/**
		 * Returns the parsed value for the argument, or throws an ArgumentException if
		 * the parsed argument does not have the given form.
		 **/
		@SuppressWarnings("unchecked")
		public <T> T getValue(String form) {
			if (!is(form))
				throw new ArgumentException(
						"The provided argument " + argument.getForms()[0] + " does not match the parsed argument " + this.argument.getForms()[0] + ".");
			return (T) value;
		}
	}

	/** Parsed arguments as returned by {@link Arguments#parse(String[])}. **/
	public static class ParsedArguments {
		private final List<ParsedArgument> parsedArguments;

		ParsedArguments(List<ParsedArgument> parsedArguments) {
			this.parsedArguments = parsedArguments;
		}

		/** Returns the parsed arguments in the order they occurred in the input. **/
		public List<ParsedArgument> getParsedArguments() {
			return parsedArguments;
		}

		/**
		 * Returns the value for the given argument, or throws an
		 * {@link ArgumentException} if the value is not found.
		 **/
		public <T> T getValue(ArgumentWithValue<T> argument) {
			for (final ParsedArgument parsedArg : parsedArguments) {
				if (parsedArg.is(argument)) {
					return parsedArg.getValue(argument);
				}
			}
			throw new ArgumentException("The argument " + argument.getForms()[0] + " was not found or does not have a value.");
		}

		/**
		 * Returns the value for the argument with the given form, or throws an
		 * {@link ArgumentException} if the value is not found.
		 **/
		public <T> T getValue(String form) {
			for (final ParsedArgument parsedArg : parsedArguments) {
				if (parsedArg.is(form)) {
					return parsedArg.getValue(form);
				}
			}
			throw new ArgumentException("The argument " + form + " was not found or does not have a value.");
		}

		/** Returns whether the argument with the given form was parsed. **/
		public boolean has(String form) {
			for (final ParsedArgument parsedArg : parsedArguments) {
				if (parsedArg.is(form))
					return true;
			}
			return false;
		}

		/** Returns whether the given argument was parsed. **/
		public boolean has(BaseArgument argument) {
			for (final ParsedArgument parsedArg : parsedArguments) {
				if (parsedArg.is(argument))
					return true;
			}
			return false;
		}
	}

	private final Set<BaseArgument> arguments = ConcurrentHashMap.newKeySet();

	private void checkDuplicateForm(BaseArgument argument) {
		for (final BaseArgument other : arguments) {
			for (final String otherForm : other.getForms()) {
				for (final String form : argument.getForms()) {
					if (otherForm.equals(form))
						throw new ArgumentException("An Argument with form " + form + " has already been added.");
				}
			}
		}
	}

	private boolean formMatches(BaseArgument argument, String formToMatch) {
		for (final String form : argument.getForms()) {
			if (form.equals(formToMatch))
				return true;
		}
		return false;
	}

	/** Adds a new {@link Argument}. **/
	public Argument addArgument(Argument argument) {
		checkDuplicateForm(argument);
		arguments.add(argument);
		return argument;
	}

	/** Adds a new {@link ArgumentWithValue}. **/
	public <T extends ArgumentWithValue<V>, V> T addArgument(T argument) {
		checkDuplicateForm(argument);
		arguments.add(argument);
		return argument;
	}

	/**
	 * Parses the given arguments by matching them with the short or long form of
	 * {@link Argument} and {@link ArgumentWithValue} instances added via
	 * {@link #addArgument(Argument)} and {@link #addArgument(ArgumentWithValue)}.
	 * In case a non-optional argument is not matched, an {@link ArgumentException}
	 * is thrown with the message describing which non-optional arguments have not
	 * been found. In case the value of an argument could not be parsed, an
	 * {@link ArgumentException} is thrown describing why the value could not be
	 * parsed. In case a value for an argument with expected value is not found, an
	 * {@link ArgumentException} is thrown, with its message describing for which
	 * argument the value could not be found.
	 */
	@SuppressWarnings(
		{ "unchecked", "rawtypes" })
	public ParsedArguments parse(String[] args) {
		final Set<BaseArgument> nonOptional = new HashSet<BaseArgument>();
		for (final BaseArgument arg : arguments) {
			if (!arg.isOptional())
				nonOptional.add(arg);
		}

		final List<ParsedArgument> parsedArguments = FastLists.list();
		int index = 0;
		while (index < args.length) {
			final String a = args[index++];

			boolean matched = false;
			for (final BaseArgument arg : arguments) {
				if (formMatches(arg, a)) {
					matched = true;

					if (!arg.isOptional())
						nonOptional.remove(arg);

					if (arg instanceof ArgumentWithValue) {
						if (index >= args.length)
							throw new ArgumentException("Expected value for argument " + a + ", but no value was given.");
						parsedArguments.add(new ParsedArgument((ArgumentWithValue) arg, ((ArgumentWithValue) arg).parseValue(args[index++])));
					} else {
						parsedArguments.add(new ParsedArgument((Argument) arg));
					}
					break;
				}
			}

			if (!matched) {
				// throw new ArgumentException("Unknown argument " + a);
				log.warn("Registered undeclared argument: " + a);
				parsedArguments.add(new ParsedArgument(new Argument(a, "%ARGUMENT_UNREGISTERED%", false)));
			}
		}

		if (nonOptional.size() > 0) {
			final StringBuilder builder = new StringBuilder(IOUtils.STRING_ALLOC_BUFFER);
			int i = 0;
			for (BaseArgument arg : nonOptional) {
				builder.append(arg.getForms()[0]);
				if (i < nonOptional.size() - 1)
					builder.append(", ");
				i++;
			}
			throw new ArgumentException("Expected the following non-optional arguments: " + builder.toString() + ".");
		}
		return new ParsedArguments(parsedArguments);
	}

	/**
	 * Outputs the help text of each argument in the order they were added with
	 * {@link #addArgument(Argument)} and
	 * {@link #addArgument(ArgumentWithValue, ArgumentWithValueMatchedCallback)}.
	 * Uses the values returned by {@link Argument#getHelpText()} and
	 * {@link ArgumentWithValue#getValueHelpText()}.
	 **/
	public void printHelp(PrintStream stream) {
		for (BaseArgument arg : arguments) {
			final String[] formTexts = new String[arg.getForms().length];
			final String[] forms = arg.getForms();

			boolean helpTextOnOwnLine = false;
			for (int i = 0, n = formTexts.length; i < n; i++) {
				String form = forms[i];
				if (arg instanceof ArgumentWithValue)
					form += " " + ((ArgumentWithValue<?>) arg).getValueHelpText();
				form = rightPad(form, 18);
				formTexts[i] = form;
				if (form.length() > 18)
					helpTextOnOwnLine = true;
			}

			if (helpTextOnOwnLine) {
				for (final String form : formTexts) {
					stream.print(form);
					stream.print("\n");
				}
				for (final String line : Splitters.splitString(arg.getHelpText(), "\n")) {
					stream.print("                  ");
					stream.print(line);
					stream.print("\n");
				}
			} else {
				final String[] lines = Splitters.splitString(arg.getHelpText(), "\n");
				for (int i = 0, n = Math.max(lines.length, forms.length); i < n; i++) {
					if (i < forms.length)
						stream.print(formTexts[i]);
					if (i >= forms.length && i < lines.length)
						stream.print("                  ");
					if (i < lines.length)
						stream.print(lines[i]);
					stream.print("\n");
				}
			}
			stream.print("\n");
		}
	}

	/**
	 * Returns the help text of each argument in the order they were added with
	 * {@link #addArgument(Argument)} and
	 * {@link #addArgument(ArgumentWithValue, ArgumentWithValueMatchedCallback)} as
	 * a String. Uses the values returned by {@link Argument#getHelpText()} and
	 * {@link ArgumentWithValue#getValueHelpText()}.
	 **/
	public String printHelp() {
		final ByteArrayOutputStream bytes = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
		final PrintStream out = new PrintStream(bytes);
		printHelp(out);
		return new String(bytes.toByteArray());
	}

	/** Pads the string with spaces to the right up until the minimum length. **/
	private String rightPad(String value, int minLength) {
		if (value.length() > minLength)
			return value;
		final StringBuilder builder = new StringBuilder(MathUtil.clamp(64, 4096, minLength - value.length()));
		builder.append(value);
		for (int i = 0, n = minLength - value.length(); i < n; i++) {
			builder.append(" ");
		}
		return builder.toString();
	}

	public static ParsedArguments setup(final String[] _args) {
		return _pargs.setAndReturnSelf(args.parse(_args)).get();
	}

	public static ParsedArguments get() {
		return _pargs.get();
	}

	public static void sysPrintHelp() {
		JVM.println(args.printHelp());
	}

	public static boolean hasArgument(final String... _args) {
//		System.out.print("hasArgument: ");
//		System.out.println(Arrays.toString(_args));
		if (_pargs == null || _pargs.isNull()) {
			// log.error("hasArgument(String...) -> _pargs == null!");
			return false;
		}
		for (final String arg : _args) {
			if (arg == null || arg.isEmpty()) {
				continue;
			}
			if (_pargs.get().has(arg))
				return true;
		}
		return false;
	}

	public static boolean notArguments(final String... _args) {
		if (_pargs == null || _pargs.isNull()) {
			// log.error("notArguments(String...) -> _pargs == null!");
			return true;
		}
		for (final String arg : _args) {
			if (arg == null || arg.isEmpty()) {
				continue;
			}
			if (_pargs.get().has(arg)) {
				return false;
			}
		}
		return true;
	}

	public static boolean isAnArgument(final Argument arg) {
		if (_pargs.isNull()) {
			// log.error("isAnArgument(Argument) -> _pargs == null!");
			return false;
		}
		return _pargs.get().has(arg);
	}

	public static boolean notAnArgument(final Argument arg) {
		if (_pargs.isNull()) {
			// log.error("notAnArgument(Argument) -> _pargs == null!");
			return true;
		}
		return !_pargs.get().has(arg);
	}

	public static ParsedArguments awaitGet() {
		if (get() != null)
			return get();
		return ExecutorManager.globalSubmit(() -> {
			ParsedArguments out = null;
			for (int i = 0; i < 5; i++) {
				final ParsedArguments g = get();
				if (g != null) {
					out = g;
					break;
				} else {
					try {
						Thread.sleep(33L);
					} catch (InterruptedException e) {
						Thread.interrupted();
					}
				}
			}
			return out;
		}).waitTillCompletion();
	}
}
